/**
 * 记录日志
 * @author 木人心
 * @version 1.1.0
 */
import { DateFormat, moment } from '@utils/moment'
import { AddLog, EditLog, LogInfo } from '@utils/log/type'
import { AxiosRequestConfig, AxiosResponse } from 'axios'
import { UserInfo } from '@typings/user'
import RejectError from '@typings/RejectError'

export enum LogLevelEnum {
  ERROR = 'error',
  WARN = 'warn',
  INFO = 'info',
  DEBUG = 'debug'
}

export class Log {
  // 数据库实例
  private static logDB: IDBDatabase
  static userInfo: UserInfo
  // 初始化/加载DB
  static loadDB (config = { tryGlobal: true }) {
    return new Promise((resolve) => {
      const indexedDB = window.indexedDB
      // 打开数据库，如果没有，则会创建
      const request = indexedDB.open('log', 1)
      // 打开成功
      request.addEventListener('success', (event) => {
        this.logDB = (event.target as IDBOpenDBRequest).result
        // 删除老旧的日志
        this.deleteOldLog()
        resolve(undefined)
      })
      // 首次创建获取版本更新后触发
      request.addEventListener('upgradeneeded', (event) => {
        // 记录数据库实例
        this.logDB = (event.target as IDBOpenDBRequest).result
        const objectStore = this.logDB.createObjectStore('log', {
          // 主键
          keyPath: 'id',
          // 实现自增
          autoIncrement: true
        })
        // 创建索引
        objectStore.createIndex('logType', 'logType', { unique: false })
        objectStore.createIndex('logLevel', 'logLevel', { unique: false })
        objectStore.createIndex('createDate', 'createDate', { unique: false })
        resolve(undefined)
      })
      if (config.tryGlobal) {
        this.addTryGlobal()
      }
    })
  }

  // region TODO 记录日志
  static info (value: Global.Object, type: string) {
    return this.addLog({ ...value, logType: type, logLevel: LogLevelEnum.INFO })
  }

  static error (value: Global.Object, type: string) {
    return this.addLog({ ...value, logType: type, logLevel: LogLevelEnum.ERROR })
  }

  static warn (value: Global.Object, type: string) {
    return this.addLog({ ...value, logType: type, logLevel: LogLevelEnum.WARN })
  }

  static debug (value: Global.Object, type: string) {
    return this.addLog({ ...value, logType: type, logLevel: LogLevelEnum.DEBUG })
  }
  // endregion

  // 添加全局异常拦截
  static addTryGlobal () {
    window.onerror = (msg, url, line, col, error) => {
      Log.error({ msg, url, line, col, error }, 'ConsoleError')
    }
    const oldError = console.error
    console.error = function (...arg: any[]) {
      Log.error({ error: arg }, 'ConsoleError')
      oldError.apply(console, arg)
    }
    window.onunhandledrejection = function (e) {
      Log.error({ error: e.reason }, 'ConsoleError')
    }
  }

  // 修改/更新日志
  static update (item: EditLog) {
    return this.editLog(item)
  }

  // 关闭数据库
  static close () {
    this.logDB.close()
  }

  // 获取日志文件（.txt）
  static getFile (fileName: string, bound?: { startTime: string, endTime: string }) {
    return new Promise<File>((resolve, reject) => {
      // 添加事务获取表实例
      const objectStore = this.logDB.transaction(['log'], 'readonly').objectStore('log')
      let request: IDBRequest<any[]> = null
      if (bound) {
        // 获取索引
        const idbIndex = objectStore.index('createDate')
        request = idbIndex.getAll(IDBKeyRange.bound(bound.startTime, bound.endTime))
      } else {
        request = objectStore.getAll()
      }
      request.onsuccess = (event) => {
        try {
          const result = (event.target as IDBRequest).result
          const file = new File([JSON.stringify(result)], fileName + '.txt', {
            type: 'text/plain'
          })
          resolve(file)
        } catch (e) {
          reject(new RejectError({
            code: 401, message: '生成文件失败', data: e
          }))
        }
      }
      request.onerror = (e) => {
        reject(new RejectError({
          code: 400, message: '获取日志失败', data: e
        }))
      }
      return new File([], fileName + '.txt', {
        type: 'text/plain'
      })
    })
  }

  // 下载日志（.txt）
  static download (fileName: string, bound?: { startTime: string, endTime: string }) {
    return new Promise((resolve, reject) => {
      this.getFile(fileName, bound).then((file) => {
        const a = document.createElement('a')
        a.download = fileName + '.txt'
        const href = URL.createObjectURL(file)
        a.href = href
        a.click()
        URL.revokeObjectURL(href)
        resolve(undefined)
      }).catch(reject)
    })
  }

  // 导入日志，为保证id主键唯一，需要先删除浏览器的日志表利用loadDB重新生成
  static importLog (file: File) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.readAsText(file)
      reader.onload = async (e) => {
        try {
          const jsonStr = e.target.result as string
          const logInfoList = JSON.parse(jsonStr) as LogInfo[]
          const objectStore = this.logDB.transaction(['log'], 'readwrite')
            .objectStore('log')
          for (let i = 0, len = logInfoList.length; i < len; i++) {
            const logInfo = logInfoList[i]
            try {
              await new Promise((resolve, reject) => {
                const request = objectStore.add(logInfo)
                request.onsuccess = () => {
                  resolve(undefined)
                }
                request.onerror = () => {
                  reject(new RejectError({
                    code: 401,
                    message: '插入日志失败，下标' + i,
                    data: logInfo
                  }))
                }
              })
            } catch (e) {
              reject(e)
              break
            }
          }
        } catch (e: any) {
          reject(new RejectError({
            code: 400,
            message: e.message,
            data: e
          }))
        }
      }
    })
  }

  private static addLog (item: AddLog) {
    return new Promise<number>((resolve) => {
      try {
        // 生成创建时间
        const m = moment()
        item.createDate = m.format(DateFormat.Date)
        item.createTime = m.format(DateFormat.DateTime)
        item.user = this.userInfo
        // 使用事务添加日志
        const request = this.logDB.transaction(['log'], 'readwrite')
          .objectStore('log')
          .add(item)
        request.onsuccess = (event) => {
          resolve((event.target as IDBRequest).result)
        }
        request.onerror = () => {
          resolve(undefined)
        }
      } catch (e) {
        resolve(undefined)
      }
    })
  }

  private static editLog (item: EditLog) {
    return new Promise<boolean>((resolve) => {
      try {
        // 生成修改时间
        item.updateTime = moment().format(DateFormat.DateTime)
        // 添加事务获取表实例
        const objectStore = this.logDB.transaction(['log'], 'readwrite')
          .objectStore('log')
        // 根据id查询获取日志
        const getRequest = objectStore.get(item.id)
        getRequest.onsuccess = (event) => {
          const log = (event.target as IDBRequest).result
          // 合并就日志数据，提交修改
          const putRequest = objectStore.put(Object.assign(log, item))
          putRequest.onsuccess = () => {
            resolve(true)
          }
          putRequest.onerror = () => {
            resolve(false)
          }
        }
        getRequest.onerror = () => {
          resolve(false)
        }
      } catch (e) {
        resolve(undefined)
      }
    })
  }

  // 删除老旧日志
  private static deleteOldLog () {
    return new Promise<boolean>((resolve) => {
      try {
        // 添加事务获取表实例
        const objectStore = this.logDB.transaction(['log'], 'readwrite').objectStore('log')
        // 获取索引
        const idbIndex = objectStore.index('createDate')
        // 生成查询范围，XX天之前的数据
        // https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange
        const getRange = IDBKeyRange.upperBound(moment().add(-30, 'day').format(DateFormat.Date))
        // 获取条件内的日志数据
        const getRequest = idbIndex.getAll(getRange)
        getRequest.onsuccess = (event) => {
          const result = (event.target as IDBRequest).result
          // 如果查询有日志
          if (result.length) {
            // 生成删除范围，delete只能按照主键删除，这里采用小于查询结果的最后一条数据的id作为删除范围，因为id是自增的
            const delRange = IDBKeyRange.upperBound(result[result.length - 1].id)
            // 删除条件内的日志数据
            const delRequest = objectStore.delete(delRange)
            delRequest.onsuccess = () => {
              resolve(true)
            }
            delRequest.onerror = () => {
              resolve(undefined)
            }
          } else {
            resolve(false)
          }
        }
        getRequest.onerror = () => {
          resolve(undefined)
        }
      } catch (e) {
        resolve(false)
      }
    })
  }
}
(window as any).Log = Log
// TODO 为request记录日志
export class RequestLog {
  static async request (req: AxiosRequestConfig<any>) {
    const { adapter, baseURL, headers, method, timeout, url, data, params } = req
    const logId = await Log.info({
      url, request: { url, data, params, method, headers, adapter, baseURL, timeout }
    }, 'Request');
    (req as any).logId = logId
    return logId
  }

  static response (res: AxiosResponse<any, any>, logLevel = LogLevelEnum.INFO) {
    const id = (res.config as any).logId
    if (id) {
      const { status, statusText, headers, data } = res
      if (res.config.dataToLog === false) delete data.payload
      Log.update({ id, response: { status, statusText, headers, data }, logLevel })
    }
  }
}
